package com.libleveldb.implementation

import android.util.Log
import com.libleveldb.Iterator
import com.libleveldb.LevelDB
import com.libleveldb.Snapshot
import com.libleveldb.WriteBatch
import com.libleveldb.exception.LevelDBClosedException
import com.libleveldb.exception.LevelDBException
import com.libleveldb.exception.LevelDBSnapshotOwnershipException

/**
 * Object for interacting with the native LevelDB implementation.
 */
class NativeLevelDB @JvmOverloads constructor(path: String, configuration: Configuration? = null) :
    LevelDB() {
    companion object {
        /**
         * @see com.libleveldb.LevelDB.destroy
         */
        @Throws(LevelDBException::class)
        fun destroy(path: String) {
            nativeDestroy(path)
        }

        /**
         * @see com.libleveldb.LevelDB.repair
         */
        @Throws(LevelDBException::class)
        fun repair(path: String) {
            nativeRepair(path)
        }

        /**
         * Natively opens the database.
         *
         * @param createIfMissing
         * @param path
         * @return the nat structure pointer
         * @throws LevelDBException
         */
        @Throws(LevelDBException::class)
        private external fun nativeOpen(
            createIfMissing: Boolean,
            cacheSize: Int,
            blockSize: Int,
            writeBufferSize: Int,
            path: String
        ): Long

        /**
         * Natively closes pointers and memory. Pointer is unchecked.
         *
         * @param ndb
         */
        private external fun nativeClose(ndb: Long)

        /**
         * Natively writes key-value pair to the database. Pointer is unchecked.
         *
         * @param ndb
         * @param sync
         * @param key
         * @param value
         * @throws LevelDBException
         */
        @Throws(LevelDBException::class)
        private external fun nativePut(ndb: Long, sync: Boolean, key: ByteArray, value: ByteArray)

        /**
         * Natively deletes key-value pair from the database. Pointer is unchecked.
         *
         * @param ndb
         * @param sync
         * @param key
         * @throws LevelDBException
         */
        @Throws(LevelDBException::class)
        private external fun nativeDelete(ndb: Long, sync: Boolean, key: ByteArray)

        @Throws(LevelDBException::class)
        private external fun nativeWrite(ndb: Long, sync: Boolean, nwb: Long)

        /**
         * Natively retrieves key-value pair from the database. Pointer is unchecked.
         *
         * @param ndb
         * @param key
         * @return
         * @throws LevelDBException
         */
        @Throws(LevelDBException::class)
        private external fun nativeGet(ndb: Long, key: ByteArray, snapshotId: Long): ByteArray

        /**
         * Natively gets LevelDB property. Pointer is unchecked.
         *
         * @param ndb
         * @param key
         * @return
         */
        private external fun nativeGetProperty(ndb: Long, key: ByteArray): ByteArray?

        /**
         * Natively destroys a database. Corresponds to: <tt>leveldb::DestroyDB()</tt>
         *
         * @param path
         * @throws LevelDBException
         */
        @Throws(LevelDBException::class)
        private external fun nativeDestroy(path: String)

        /**
         * Natively repairs a database. Corresponds to: <tt>leveldb::RepairDB()</tt>
         *
         * @param path
         * @throws LevelDBException
         */
        @Throws(LevelDBException::class)
        private external fun nativeRepair(path: String)

        /**
         * Natively creates a new iterator. Corresponds to <tt>leveldb::DB->NewIterator()</tt>.
         *
         * @param ndb
         * @param fillCache
         * @return
         */
        private external fun nativeIterate(ndb: Long, fillCache: Boolean, snapshotId: Long): Long
        private external fun nativeSnapshot(ndb: Long): Long
        private external fun nativeReleaseSnapshot(ndb: Long, snapshotId: Long)

        init {
            System.loadLibrary("leveldb-android")
        }
    }

    // This is the underlying pointer. If you touch this, all hell breaks loose and everyone dies.
    @Volatile
    private var ndb: Long

    /**
     * The path that this database has been opened with.
     *
     * @return the path
     */
    @Volatile
    override var path: String? = null

    /**
     * Closes this database, i.e. releases nat resources. You may call this multiple times. You cannot use any other
     * method on this object after closing it.
     */
    override fun close() {
        var closeMultiple = false
        synchronized(this) {
            if (ndb != 0L) {
                nativeClose(ndb)
                ndb = 0
            } else {
                closeMultiple = true
            }
        }
        if (closeMultiple) {
            Log.i(NativeLevelDB::class.java.name, "Trying to close database multiple times.")
        }
    }

    /**
     * Writes a key-value record to the database. Wirting can be synchronous or asynchronous.
     *
     * Asynchronous writes will be buffered to the kernel before this function returns. This guarantees data consistency
     * even if the process crashes or is killed, but not if the system crashes.
     *
     * Synchronous writes block everything until data gets written to disk. Data is secure even if the system crashes.
     *
     * @param key   the key (usually a string, but bytes are the way LevelDB stores things)
     * @param value the value
     * @param sync  whether this is a synchronous (true) or asynchronous (false) write
     * @throws LevelDBException
     */
    @Throws(LevelDBException::class)
    override fun put(key: ByteArray?, value: ByteArray?, sync: Boolean) {
        if (value == null) {
            del(key, sync)
            return
        }
        requireNotNull(key) { "Key must not be null!" }
        synchronized(this) {
            checkIfClosed()
            nativePut(ndb, sync, key, value)
        }
    }

    /**
     * Writes a [com.libleveldb.WriteBatch] to the database.
     *
     * @param writeBatch the WriteBatch to write
     * @param sync       whether this is a synchronous (true) or asynchronous (false) write
     * @throws LevelDBException
     */
    @Throws(LevelDBException::class)
    override fun write(writeBatch: WriteBatch?, sync: Boolean) {
        requireNotNull(writeBatch) { "Write batch must not be null." }
        synchronized(this) {
            checkIfClosed()
            val nativeWriteBatch = NativeWriteBatch(writeBatch)
            nativeWriteBatch.use {
                nativeWrite(ndb, sync, it.nativePointer())
            }
        }
    }

    /**
     * Gets the value associated with the key, or <tt>null</tt>.
     *
     * @param key the key
     * @param snapshot the snapshot from which to read the pair, or null
     * @return the value, or <tt>null</tt>
     * @throws LevelDBException
     */
    @Throws(LevelDBSnapshotOwnershipException::class, LevelDBException::class)
    override fun get(key: ByteArray?, snapshot: Snapshot?): ByteArray {
        requireNotNull(key) { "Key must not be null!" }
        if (snapshot != null) {
            if (snapshot !is NativeSnapshot) {
                throw LevelDBSnapshotOwnershipException()
            }
            if (!snapshot.checkOwner(this)) {
                throw LevelDBSnapshotOwnershipException()
            }
        }
        synchronized(this) {
            checkIfClosed()
            return nativeGet(
                ndb,
                key,
                if (snapshot == null) 0 else (snapshot as NativeSnapshot).id()
            )
        }
    }

    /**
     * Deletes the specified entry from the database. Deletion can be synchronous or asynchronous.
     *
     * @param key  the key
     * @param sync whether this is a synchronous (true) or asynchronous (false) delete
     * @throws LevelDBException
     */
    @Throws(LevelDBException::class)
    override fun del(key: ByteArray?, sync: Boolean) {
        requireNotNull(key) { "Key must not be null." }
        synchronized(this) {
            checkIfClosed()
            nativeDelete(ndb, sync, key)
        }
    }

    /**
     * Get a property of LevelDB, or null.
     *
     * Valid property names include:
     *
     *   * "leveldb.num-files-at-level<N>" - return the number of files at level <N>, where <N> is an ASCII
     * representation of a level number (e.g. "0").</N></N></N>
     *
     *  * "leveldb.stats" - returns a multi-line string that describes statistics about the internal operation of the
     * DB.
     *
     *  * "leveldb.sstables" - returns a multi-line string that describes all of the sstables that make up the db
     * contents.
     *
     *
     *
     * @param key the key
     * @return property data, or <tt>null</tt>
     * @throws LevelDBClosedException
     */
    @Throws(LevelDBClosedException::class)
    override fun getPropertyBytes(key: ByteArray?): ByteArray? {
        requireNotNull(key) { "Key must not be null." }
        synchronized(this) {
            checkIfClosed()
            return nativeGetProperty(ndb, key)
        }
    }

    /**
     * Creates a new [com.libleveldb.Iterator] that iterates over this database.
     *
     * The returned iterator is not thread safe and must be closed with [com.libleveldb.Iterator.close] before closing this
     * database.
     *
     * @param fillCache whether iterating fills the internal cache
     * @return a new iterator
     * @throws LevelDBClosedException
     */
    @Throws(LevelDBSnapshotOwnershipException::class, LevelDBClosedException::class)
    override fun iterator(fillCache: Boolean, snapshot: Snapshot?): Iterator {
        if (snapshot != null) {
            if (snapshot !is NativeSnapshot) {
                throw LevelDBSnapshotOwnershipException()
            }
            if (snapshot.checkOwner(this)) {
                throw LevelDBSnapshotOwnershipException()
            }
        }
        synchronized(this) {
            checkIfClosed()
            return NativeIterator(
                nativeIterate(
                    ndb,
                    fillCache,
                    if (snapshot == null) 0 else (snapshot as NativeSnapshot).id()
                )
            )
        }
    }

    /**
     * Checks whether this database has been closed.
     *
     * @return true if closed, false if not
     */
    override val isClosed: Boolean
        get() = ndb == 0L

    @Throws(LevelDBClosedException::class)
    override fun obtainSnapshot(): Snapshot {
        return NativeSnapshot(this, nativeSnapshot(ndb))
    }

    @Throws(LevelDBSnapshotOwnershipException::class, LevelDBClosedException::class)
    override fun releaseSnapshot(snapshot: Snapshot?) {
        requireNotNull(snapshot) { "Snapshot must not be null." }
        if (snapshot !is NativeSnapshot) {
            throw LevelDBSnapshotOwnershipException()
        }
        if (!snapshot.checkOwner(this)) {
            throw LevelDBSnapshotOwnershipException()
        }
        synchronized(this) {
            checkIfClosed()
            nativeReleaseSnapshot(ndb, snapshot.release())
        }
    }

    /**
     * Checks if this database has been closed. If it has, throws a [com.libleveldb.exception.LevelDBClosedException].
     *
     * Use before calling any of the nat functions that require the ndb pointer.
     *
     * Don't call this outside a synchronized context.
     *
     * @throws LevelDBClosedException
     */
    @Throws(LevelDBClosedException::class)
    fun checkIfClosed() {
        if (isClosed) {
            throw LevelDBClosedException()
        }
    }

    /**
     * Opens a new LevelDB database, and creates it if missing.
     */
    init {
        val config = configuration ?: configure()
        ndb = nativeOpen(
            config.createIfMissing(),
            config.cacheSize(),
            config.blockSize(),
            config.writeBufferSize(),
            path
        )
        this.path = path
    }
}